package com.luych.toolbox.docker.service.service;

import com.luych.toolbox.common.enums.TaskStatus;
import com.luych.toolbox.common.enums.TaskType;
import com.luych.toolbox.common.feign.intf.base.service.TaskFeign;
import com.luych.toolbox.common.feign.intf.gateway.center.FileFeign;
import com.luych.toolbox.common.feign.param.base.service.TaskParam;
import com.luych.toolbox.common.feign.param.docker.service.*;
import com.luych.toolbox.common.feign.view.base.service.TaskView;
import com.luych.toolbox.common.feign.view.base.service.UserView;
import com.luych.toolbox.common.feign.view.docker.service.DockerImageHistoryView;
import com.luych.toolbox.common.feign.view.docker.service.DockerImageView;
import com.luych.toolbox.common.page.CommonPageResult;
import com.luych.toolbox.common.service.exception.HttpException;
import com.luych.toolbox.common.service.security.SecurityUtil;
import com.luych.toolbox.common.service.tool.Capacity;
import com.luych.toolbox.docker.service.constant.Constant;
import com.luych.toolbox.docker.service.dao.DockerRegistryDao;
import com.luych.toolbox.docker.service.dao.DockerServerDao;
import com.luych.toolbox.docker.service.entity.DockerRegistry;
import com.luych.toolbox.docker.service.entity.DockerServer;
import com.luych.toolbox.docker.service.job.DockerImageBuildJob;
import com.luych.toolbox.docker.service.job.DockerImageLoadJob;
import com.luych.toolbox.docker.service.job.DockerImagePullJob;
import com.luych.toolbox.docker.service.job.DockerImagePushJob;
import com.luych.toolbox.docker.service.sdk.DockerClient;
import com.luych.toolbox.docker.service.sdk.image.model.DockerImageDeleteResult;
import com.luych.toolbox.docker.service.sdk.image.model.DockerImageHistoryResult;
import com.luych.toolbox.docker.service.sdk.image.model.DockerImageInspectResult;
import com.luych.toolbox.docker.service.sdk.image.model.DockerImagesResult;
import org.quartz.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ContentDisposition;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.client.ResponseExtractor;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.*;

@Service
public class DockerImageService {
    private static final Logger logger = LoggerFactory.getLogger(DockerImageService.class);

    private final Scheduler scheduler;

    private final DockerServerDao dockerServerDao;
    private final DockerRegistryDao dockerRegistryDao;

    private final FileFeign fileFeign;
    private final TaskFeign taskFeign;

    @Autowired
    public DockerImageService(Scheduler scheduler, DockerServerDao dockerServerDao,
                              DockerRegistryDao dockerRegistryDao,
                              FileFeign fileFeign,
                              TaskFeign taskFeign) {
        this.scheduler = scheduler;
        this.dockerServerDao = dockerServerDao;
        this.dockerRegistryDao = dockerRegistryDao;
        this.fileFeign = fileFeign;
        this.taskFeign = taskFeign;
    }

    private List<DockerImageView> convert(DockerImagesResult dockerImagesResult) {
        List<DockerImageView> dockerImageViews = new ArrayList<>();
        for (DockerImagesResult.Image dockerImageResult : dockerImagesResult) {
            for (String dockerImageTag : dockerImageResult.getRepoTags()) {
                DockerImageView dockerImageView = new DockerImageView();
                dockerImageView.setId(dockerImageResult.getId());
                dockerImageView.setName(dockerImageTag.split(":")[0]);
                dockerImageView.setTag(dockerImageTag.split(":")[1]);
                dockerImageView.setSize(Capacity.convertHumanReadSimple(dockerImageResult.getSize()));
                dockerImageView.setCreated(dockerImageResult.getCreatedDate());
                dockerImageViews.add(dockerImageView);
            }
        }
        return dockerImageViews;
    }

    private DockerImageView convert(DockerImageInspectResult dockerImageInspectResult) {
        DockerImageView dockerImageView = new DockerImageView();
        dockerImageView.setId(dockerImageInspectResult.getId());
        //dockerImageView.setName(dockerImageTag.split(":")[0]);
        //dockerImageView.setTag(dockerImageTag.split(":")[1]);
        dockerImageView.setSize(Capacity.convertHumanReadSimple(dockerImageInspectResult.getSize()));
        dockerImageView.setCreated(dockerImageInspectResult.getCreatedDate());
        dockerImageView.setOs(dockerImageInspectResult.getOs());
        dockerImageView.setArchitecture(dockerImageInspectResult.getArchitecture());
        dockerImageView.setEnv(dockerImageInspectResult.getConfig().getEnv());
        dockerImageView.setCmd(dockerImageInspectResult.getConfig().getCmd());
        dockerImageView.setEntrypoint(dockerImageInspectResult.getConfig().getEntrypoint());
        return dockerImageView;
    }

    private List<DockerImageHistoryView> convert(DockerImageHistoryResult dockerImageHistoryResult) {
        List<DockerImageHistoryView> dockerImageHistoryViews = new ArrayList<>();
        for (DockerImageHistoryResult.History history : dockerImageHistoryResult) {
            DockerImageHistoryView dockerImageHistoryView = new DockerImageHistoryView();
            dockerImageHistoryView.setId(history.getId());
            dockerImageHistoryView.setSize(Capacity.convertHumanReadSimple(history.getSize()));
            dockerImageHistoryView.setCreated(history.getCreatedDate());
            dockerImageHistoryView.setCreatedBy(history.getCreatedBy());
            dockerImageHistoryView.setComment(history.getComment());
            dockerImageHistoryViews.add(dockerImageHistoryView);
        }
        return dockerImageHistoryViews;
    }

    public CommonPageResult<DockerImageView> page(long pageSize, long pageNumber, String sort, String order, String serverId, String keyword) {
        Assert.notNull(serverId, "Param serverId can't be empty");
        DockerServer dockerServer = dockerServerDao.findById(serverId).orElseThrow(() ->
                new HttpException(HttpStatus.BAD_REQUEST, Constant.DOCKER_SERVER_NOT_FOUND)
        );
        DockerClient dockerClient = new DockerClient(dockerServer.getUrl());
        DockerImagesResult dockerImagesResult = dockerClient.images().all();
        List<DockerImageView> dockerImageViews = this.convert(dockerImagesResult);
        if (StringUtils.hasText(keyword)) {
            dockerImageViews.removeIf(v -> !v.getName().contains(keyword) && !v.getTag().contains(keyword));
        }
        dockerImageViews.sort(Comparator.comparing(DockerImageView::getCreated));
        int from = (int) (pageSize * (pageNumber - 1));
        int to = (int) (pageSize * pageNumber);
        to = Math.min(to, dockerImageViews.size());
        return new CommonPageResult<>(dockerImageViews.subList(from, to), dockerImageViews.size());
    }

    private JobKey getJobKey(String id, Class<? extends Job> clazz) {
        return new JobKey(id, clazz.getName());
    }

    private JobDetail getJobDetail(String id, Class<? extends Job> clazz) {
        Map<String, Object> jobParam = new HashMap<String, Object>() {{
            this.put("id", id);
        }};
        return JobBuilder.newJob(clazz)
                .withIdentity(this.getJobKey(id, clazz))
                .usingJobData(new JobDataMap(jobParam))
                .build();
    }

    private Trigger getJobTrigger(String id, Class<? extends Job> clazz) {
        return TriggerBuilder.newTrigger()
                .withIdentity(id, clazz.getName())
                .withSchedule(SimpleScheduleBuilder.repeatSecondlyForTotalCount(1, 1))
                .build();
    }

    private void tag(DockerServer server, DockerImageTagParam tag) {
        DockerClient client = new DockerClient(server.getUrl());
        client.images().image(tag.getId()).tag(tag.getName(), tag.getTag());
    }

    private void pull(DockerServer server, DockerImagePullParam pull) {
        String user = null, pass = null, t = pull.getName() + ":" + pull.getTag();
        if (pull.getRegistryId() != null) {
            DockerRegistry dockerRegistry = dockerRegistryDao.findById(pull.getRegistryId()).orElseThrow(() ->
                    new HttpException(HttpStatus.BAD_REQUEST, Constant.DOCKER_REGISTRY_NOT_FOUND)
            );
            user = dockerRegistry.getUser();
            pass = dockerRegistry.getPass();
            t = dockerRegistry.getUrl() + "/" + pull.getName() + ":" + pull.getTag();
        }

        DockerImagePullJob.Param stdin = new DockerImagePullJob.Param();
        stdin.setServer(server);
        stdin.setImage(t);
        stdin.setUser(user);
        stdin.setPass(pass);

        UserView session = SecurityUtil.getSession();

        TaskParam taskParam = new TaskParam();
        taskParam.setType(TaskType.DOCKER_IMAGE_PULL);
        taskParam.setStatus(TaskStatus.PENDING);
        taskParam.setName("拉取容器镜像");
        taskParam.setDescription("在宿主机[" + server.getName() + "]上拉取容器镜像[" + t +"]");
        taskParam.setStdin(stdin.toJson());
        taskParam.setOwnerId(session.getId());
        TaskView taskResult = taskFeign.create(taskParam);

        try {
            scheduler.scheduleJob(
                    this.getJobDetail(taskResult.getId(), DockerImagePullJob.class),
                    this.getJobTrigger(taskResult.getId(), DockerImagePullJob.class)
            );
        } catch (SchedulerException e) {
            throw new IllegalStateException(e);
        }
    }

    private void load(DockerServer server, DockerImageLoadParam load) {
        DockerImageLoadJob.Param stdin = new DockerImageLoadJob.Param();
        stdin.setServer(server);
        stdin.setFile(load.getFile());

        UserView session = SecurityUtil.getSession();

        TaskParam taskParam = new TaskParam();
        taskParam.setType(TaskType.DOCKER_IMAGE_LOAD);
        taskParam.setStatus(TaskStatus.PENDING);
        taskParam.setName("上传容器镜像");
        taskParam.setDescription("在宿主机[" + server.getName() + "]上传容器镜像");
        taskParam.setStdin(stdin.toJson());
        taskParam.setOwnerId(session.getId());
        TaskView taskResult = taskFeign.create(taskParam);

        fileFeign.confirm(load.getFile());

        try {
            scheduler.scheduleJob(
                    this.getJobDetail(taskResult.getId(), DockerImageLoadJob.class),
                    this.getJobTrigger(taskResult.getId(), DockerImageLoadJob.class)
            );
        } catch (SchedulerException e) {
            throw new IllegalStateException(e);
        }
    }

    private void build(DockerServer server, DockerImageBuildParam build) {
        String registry = null, user = null, pass = null;
        if (build.getRegistryId() != null) {
            DockerRegistry dockerRegistry = dockerRegistryDao.findById(build.getRegistryId()).orElseThrow(() ->
                    new HttpException(HttpStatus.BAD_REQUEST, Constant.DOCKER_REGISTRY_NOT_FOUND)
            );
            registry = dockerRegistry.getName();
            user = dockerRegistry.getUser();
            pass = dockerRegistry.getPass();
        }
        String image = build.getName() + ":" + build.getTag();

        UserView session = SecurityUtil.getSession();

        DockerImageBuildJob.Param stdin = new DockerImageBuildJob.Param();
        stdin.setServer(server);
        stdin.setFile(build.getFile());
        stdin.setImage(image);
        stdin.setRegistry(registry);
        stdin.setUser(user);
        stdin.setPass(pass);

        TaskParam taskParam = new TaskParam();
        taskParam.setType(TaskType.DOCKER_IMAGE_BUILD);
        taskParam.setStatus(TaskStatus.PENDING);
        taskParam.setName("构建容器镜像");
        taskParam.setDescription("在宿主机[" + server.getName() + "]构建容器镜像");
        taskParam.setStdin(stdin.toJson());
        taskParam.setOwnerId(session.getId());
        TaskView taskResult = taskFeign.create(taskParam);

        fileFeign.confirm(build.getFile());

        try {
            scheduler.scheduleJob(
                    this.getJobDetail(taskResult.getId(), DockerImageBuildJob.class),
                    this.getJobTrigger(taskResult.getId(), DockerImageBuildJob.class)
            );
        } catch (SchedulerException e) {
            throw new IllegalStateException(e);
        }
    }

    public void create(String serverId, DockerImageParam model) {
        Assert.notNull(serverId, "Param serverId can't be empty");
        DockerServer dockerServer = dockerServerDao.findById(serverId).orElseThrow(() ->
                new HttpException(HttpStatus.BAD_REQUEST, Constant.DOCKER_SERVER_NOT_FOUND)
        );
        if (model.getLoad() != null) {
            this.load(dockerServer, model.getLoad());
        } else if (model.getBuild() != null) {
            this.build(dockerServer, model.getBuild());
        }  else if (model.getPull() != null) {
            this.pull(dockerServer, model.getPull());
        } else if (model.getTag() != null) {
            this.tag(dockerServer, model.getTag());
        }
    }

    public void remove(String serverId, String id) {
        Assert.notNull(serverId, "Param serverId can't be empty");
        Assert.notNull(id, "Param id can't be empty");
        DockerServer dockerServer = dockerServerDao.findById(serverId).orElseThrow(() ->
                new HttpException(HttpStatus.BAD_REQUEST, Constant.DOCKER_SERVER_NOT_FOUND)
        );
        DockerClient dockerClient = new DockerClient(dockerServer.getUrl());
        DockerImageDeleteResult deleteResult = dockerClient.images().image(id).delete();
        for (HashMap<String, String> deleteMap : deleteResult) {
            for (Map.Entry<String, String> deleteEntry : deleteMap.entrySet()) {
                logger.info("{}: {}", deleteEntry.getKey(), deleteEntry.getValue());
            }
        }
    }

    public DockerImageView get(String serverId, String id) {
        Assert.notNull(serverId, "Param serverId can't be empty");
        Assert.notNull(id, "Param id can't be empty");
        DockerServer dockerServer = dockerServerDao.findById(serverId).orElseThrow(() ->
                new HttpException(HttpStatus.BAD_REQUEST, Constant.DOCKER_SERVER_NOT_FOUND)
        );
        DockerClient dockerClient = new DockerClient(dockerServer.getUrl());
        DockerImageInspectResult dockerImageInspectResult = dockerClient.images().image(id).inspect();
        DockerImageHistoryResult dockerImageHistoryResult = dockerClient.images().image(id).history();
        DockerImageView dockerImageView = this.convert(dockerImageInspectResult);
        dockerImageView.setHistories(this.convert(dockerImageHistoryResult));
        return dockerImageView;
    }

    public void download(String serverId, String id, HttpServletResponse servletResponse) {
        Assert.notNull(serverId, "Param serverId can't be empty");
        Assert.notNull(id, "Param id can't be empty");
        DockerServer dockerServer = dockerServerDao.findById(serverId).orElseThrow(() ->
                new HttpException(HttpStatus.BAD_REQUEST, Constant.DOCKER_SERVER_NOT_FOUND)
        );
        DockerClient dockerClient = new DockerClient(dockerServer.getUrl());
        ResponseExtractor<?> responseExtractor = httpResponse -> {
            InputStream inputStream = httpResponse.getBody();
            ServletOutputStream outputStream = servletResponse.getOutputStream();
            byte[] buffer = new byte[1024];
            int length;
            while ((length = inputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, length);
            }
            outputStream.flush();
            outputStream.close();
            return Object.class;
        };
        ContentDisposition disposition = ContentDisposition.builder("attachment")
                .filename(UUID.randomUUID().toString(), StandardCharsets.UTF_8).build();
        servletResponse.setHeader(HttpHeaders.CONTENT_DISPOSITION, disposition.toString());
        servletResponse.setStatus(HttpStatus.OK.value());
        dockerClient.images().image(id).get(responseExtractor);
    }

    public void push(String serverId, String id) {
        Assert.notNull(serverId, "Param serverId can't be empty");
        Assert.notNull(id, "Param id can't be empty");
        DockerServer server = dockerServerDao.findById(serverId).orElseThrow(() ->
                new HttpException(HttpStatus.BAD_REQUEST, Constant.DOCKER_SERVER_NOT_FOUND)
        );

        String user = null, pass = null, host = id.substring(0, id.indexOf("/"));
        List<DockerRegistry> registries = dockerRegistryDao.findAll();
        for (DockerRegistry registry : registries) {
            if (registry.getUrl().equals(host)) {
                user = registry.getUser();
                pass = registry.getPass();
                break;
            }
        }

        DockerImagePushJob.Param stdin = new DockerImagePushJob.Param();
        stdin.setServer(server);
        stdin.setImage(id);
        stdin.setUser(user);
        stdin.setPass(pass);

        UserView session = SecurityUtil.getSession();

        TaskParam taskParam = new TaskParam();
        taskParam.setType(TaskType.DOCKER_IMAGE_PUSH);
        taskParam.setStatus(TaskStatus.PENDING);
        taskParam.setName("推送容器镜像");
        taskParam.setDescription("在宿主机[" + server.getName() + "]上推送容器镜像[" + id +"]");
        taskParam.setStdin(stdin.toJson());
        taskParam.setOwnerId(session.getId());
        TaskView taskResult = taskFeign.create(taskParam);

        try {
            scheduler.scheduleJob(
                    this.getJobDetail(taskResult.getId(), DockerImagePushJob.class),
                    this.getJobTrigger(taskResult.getId(), DockerImagePushJob.class)
            );
        } catch (SchedulerException e) {
            throw new IllegalStateException(e);
        }
    }
}
